/*
 * Copyright 2019 Web3 Labs Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package org.web3j.protocol.http;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import okhttp3.CipherSuite;
import okhttp3.ConnectionSpec;
import okhttp3.Headers;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import okhttp3.logging.HttpLoggingInterceptor;
import okio.Buffer;
import okio.BufferedSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.web3j.protocol.Service;
import org.web3j.protocol.exceptions.ClientConnectionException;

import static okhttp3.ConnectionSpec.CLEARTEXT;

/** HTTP implementation of our services API. */
public class HttpService extends Service {

	/** Copied from {@link ConnectionSpec#APPROVED_CIPHER_SUITES}. */
	@SuppressWarnings("JavadocReference")
	private static final CipherSuite[] INFURA_CIPHER_SUITES = new CipherSuite[] {
			CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
			CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
			CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
			CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,

			// Note that the following cipher suites are all on HTTP/2's bad cipher suites
			// list.
			// We'll
			// continue to include them until better suites are commonly available. For
			// example,
			// none
			// of the better cipher suites listed above shipped with Android 4.4 or Java 7.
			CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
			CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
			CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256, CipherSuite.TLS_RSA_WITH_AES_256_GCM_SHA384,
			CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA, CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA,
			CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA,

			// Additional INFURA CipherSuites
			CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384,
			CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA256, CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA256 };

	private static final ConnectionSpec INFURA_CIPHER_SUITE_SPEC = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
			.cipherSuites(INFURA_CIPHER_SUITES).build();

	/** The list of {@link ConnectionSpec} instances used by the connection. */
	private static final List<ConnectionSpec> CONNECTION_SPEC_LIST = Arrays.asList(INFURA_CIPHER_SUITE_SPEC, CLEARTEXT);

	public static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8");

	public static final String DEFAULT_URL = "http://localhost:8545/";

	private static final Logger log = LoggerFactory.getLogger(HttpService.class);

	private OkHttpClient httpClient;

	private final String url;

	private final boolean includeRawResponse;

	private HashMap<String, String> headers = new HashMap<>();

	public HttpService(String url, OkHttpClient httpClient, boolean includeRawResponses) {
		super(includeRawResponses);
		this.url = url;
		this.httpClient = httpClient;
		this.includeRawResponse = includeRawResponses;
	}

	public HttpService(OkHttpClient httpClient, boolean includeRawResponses) {
		this(DEFAULT_URL, httpClient, includeRawResponses);
	}

	public HttpService(String url, OkHttpClient httpClient) {
		this(url, httpClient, false);
	}

	public HttpService(String url) {
		this(url, createOkHttpClient());
	}

	public HttpService(String url, boolean includeRawResponse) {
		this(url, createOkHttpClient(), includeRawResponse);
	}

	public HttpService(OkHttpClient httpClient) {
		this(DEFAULT_URL, httpClient);
	}

	public HttpService(boolean includeRawResponse) {
		this(DEFAULT_URL, includeRawResponse);
	}

	public HttpService() {
		this(DEFAULT_URL);
	}

	public static OkHttpClient.Builder getOkHttpClientBuilder() {
		final OkHttpClient.Builder builder = new OkHttpClient.Builder().connectionSpecs(CONNECTION_SPEC_LIST);
		configureLogging(builder);
		return builder;
	}

	private static OkHttpClient createOkHttpClient() {
		return getOkHttpClientBuilder().build();
	}

	private static void configureLogging(OkHttpClient.Builder builder) {
		if (log.isDebugEnabled()) {
			HttpLoggingInterceptor logging = new HttpLoggingInterceptor(log::debug);
			logging.setLevel(HttpLoggingInterceptor.Level.BODY);
			builder.addInterceptor(logging);
		}
	}

	@Override
	protected InputStream performIO(String request) throws IOException {

//		RequestBody requestBody = RequestBody.create(request, JSON_MEDIA_TYPE);
		RequestBody requestBody = RequestBody.create(JSON_MEDIA_TYPE, request);
		Headers headers = buildHeaders();

		okhttp3.Request httpRequest = new okhttp3.Request.Builder().url(url).headers(headers).post(requestBody).build();

		try (okhttp3.Response response = httpClient.newCall(httpRequest).execute()) {
			processHeaders(response.headers());
			ResponseBody responseBody = response.body();
			if (response.isSuccessful()) {
				if (responseBody != null) {
					return buildInputStream(responseBody);
				} else {
					return null;
				}
			} else {
				int code = response.code();
				String text = responseBody == null ? "N/A" : responseBody.string();

				throw new ClientConnectionException("Invalid response received: " + code + "; " + text);
			}
		}
	}

	protected void processHeaders(Headers headers) {
		// Default implementation is empty
	}

	private InputStream buildInputStream(ResponseBody responseBody) throws IOException {
		if (includeRawResponse) {
			// we have to buffer the entire input payload, so that after processing
			// it can be re-read and used to populate the rawResponse field.

			BufferedSource source = responseBody.source();
			source.request(Long.MAX_VALUE); // Buffer the entire body
			Buffer buffer = source.getBuffer();

			long size = buffer.size();
			if (size > Integer.MAX_VALUE) {
				throw new UnsupportedOperationException("Non-integer input buffer size specified: " + size);
			}

			int bufferSize = (int) size;
			InputStream inputStream = responseBody.byteStream();

			BufferedInputStream bufferedinputStream = new BufferedInputStream(inputStream, bufferSize);

			bufferedinputStream.mark(inputStream.available());
			return bufferedinputStream;

		} else {
			return new ByteArrayInputStream(responseBody.bytes());
		}
	}

	private Headers buildHeaders() {
		return Headers.of(headers);
	}

	public void addHeader(String key, String value) {
		headers.put(key, value);
	}

	public void addHeaders(Map<String, String> headersToAdd) {
		headers.putAll(headersToAdd);
	}

	public HashMap<String, String> getHeaders() {
		return headers;
	}

	public String getUrl() {
		return url;
	}

	@Override
	public void close() throws IOException {
	}
}
